﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Renci.iRods.Common;
using Renci.iRods.Exceptions;
using Renci.iRods.Packets;
using System.IO;
using System.Threading;
using System.Net.Sockets;
using System.Net;

namespace Renci.iRods.Data
{
    public class IRodsFile : FileSystemDataItem
    {
        [iRodsColumn(DataObjectAttributes.COL_D_DATA_ID)]
        public long ID { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_COLL_ID)]
        public long CollectionID { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_COLL_NAME)]
        public string CollectionName { get; internal set; }

        /// <summary>
        /// Gets the name of the file.
        /// </summary>
        [iRodsColumn(DataObjectAttributes.COL_DATA_NAME)]
        public override string Name { get; internal set; }

        private string _fullName;
        /// <summary>
        /// Gets the full name of the file.
        /// </summary>
        public override string FullName
        {
            get
            {
                if (this._fullName == null)
                {
                    this._fullName = string.Format("{0}/{1}", this.CollectionName, this.Name);
                }
                return this._fullName;
            }

            internal set
            {
                this._fullName = value;
                this.CollectionName = this.GetFullPath(this._fullName);
                this.Name = System.IO.Path.GetFileName(this._fullName);

                if (string.IsNullOrEmpty(this.CollectionName))
                {
                    this.CollectionName = this.Connection.HomeDirectory;
                }
            }
        }

        [iRodsColumn(DataObjectAttributes.COL_DATA_REPL_NUM)]
        public int ReplNum { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_DATA_VERSION)]
        public string Version { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_DATA_TYPE_NAME)]
        public string TypeName { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_DATA_SIZE)]
        public long Size { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_RESC_GROUP_NAME)]
        public string ResourceGroupName { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_RESC_NAME)]
        public string ResourceName { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_DATA_PATH)]
        public string Path { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_OWNER_NAME)]
        public string OwnerName { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_OWNER_ZONE)]
        public string OwnerZone { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_REPL_STATUS)]
        public string ReplicationStatus { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_DATA_STATUS)]
        public string Status { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_DATA_CHECKSUM)]
        public string Checksum { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_EXPIRY)]
        public string Expire { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_MAP_ID)]
        public long MapID { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_CREATE_TIME)]
        public override DateTime CreationTime { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_D_MODIFY_TIME)]
        public override DateTime LastWriteTime { get; internal set; }

        [iRodsColumn(DataObjectAttributes.COL_DATA_MODE)]
        public string Mode { get; internal set; }

        public bool IsTrash
        {
            get
            {
                if (!this.CollectionName.StartsWith("/"))
                    return false;

                if (this.CollectionName.Contains("/trash/"))
                    return true;
                else
                    return false;
            }
        }

        public IRodsCollection Collection { get; private set; }

        protected override string MetadataFullName
        {
            get { return this.FullName; }
        }

        protected override IRodsQuery MetadataQuery
        {
            get
            {
                var query = new IRodsQuery();
                query.AddOutputColumn(DataObjectAttributes.COL_META_DATA_ATTR_ID);
                query.AddOutputColumn(DataObjectAttributes.COL_META_DATA_ATTR_NAME);
                query.AddOutputColumn(DataObjectAttributes.COL_META_DATA_ATTR_VALUE);
                query.AddOutputColumn(DataObjectAttributes.COL_META_DATA_ATTR_UNITS);
                query.AddCondition(DataObjectAttributes.COL_DATA_NAME, string.Format("='{0}'", this.Name));
                query.AddCondition(DataObjectAttributes.COL_D_COLL_ID, string.Format("='{0}'", this.CollectionID));

                return query;
            }
        }

        protected override Func<IRodsDataReader, MetaData> MetadataProjector
        {
            get
            {
                return (r) =>
                {
                    return new MetaData(
                        r.GetInt32(DataObjectAttributes.COL_META_DATA_ATTR_ID),
                        r.GetString(DataObjectAttributes.COL_META_DATA_ATTR_NAME),
                        r.GetString(DataObjectAttributes.COL_META_DATA_ATTR_VALUE),
                        r.GetString(DataObjectAttributes.COL_META_DATA_ATTR_UNITS));
                };
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="IRodsFile"/> class.
        /// </summary>
        /// <param name="connection">The connection.</param>
        internal IRodsFile(IRodsConnection connection)
            : this(connection, string.Empty)
        {

        }

        /// <summary>
        /// Initializes a new instance of the <see cref="IRodsFile"/> class.
        /// </summary>
        /// <param name="connection">The connection.</param>
        /// <param name="fileName">Name of the file.</param>
        public IRodsFile(IRodsConnection connection, string fullName)
            : base(connection, fullName, "-d")
        {
        }

        /// <summary>
        /// Creates a file.
        /// </summary>
        /// <returns>A new file.</returns>
        public IRodsStream Create()
        {
            return new IRodsStream(this.Connection, this.FullName, FileMode.Create, FileAccess.ReadWrite);
        }

        /// <summary>
        /// Opens a file in the specified mode with read, write, or read/write access.
        /// </summary>
        /// <param name="mode">A <see cref="System.IO.FileMode"/> constant specifying the mode (for example, Open or Append) in which to open the file.</param>
        /// <returns>A <see cref="IRodsStream"/> object opened in the specified mode and access</returns>
        public IRodsStream Open(FileMode mode)
        {
            return this.Open(mode, FileAccess.ReadWrite);
        }

        /// <summary>
        /// Opens a file in the specified mode with read, write, or read/write access.
        /// </summary>
        /// <param name="mode">A <see cref="System.IO.FileMode"/> constant specifying the mode (for example, Open or Append) in which to open the file.</param>
        /// <param name="access">A <see cref="System.IO.FileAccess"/> constant specifying whether to open the file with Read, Write, or ReadWrite file access.</param>
        /// <returns>A <see cref="IRodsStream"/> object opened in the specified mode and access</returns>
        public IRodsStream Open(FileMode mode, FileAccess access)
        {
            return new IRodsStream(this.Connection, this.FullName, mode, access);
        }

        /// <summary>
        /// Creates a read-only <see cref="IRodsStream"/>.
        /// </summary>
        /// <returns>A new read-only <see cref="IRodsStream"/> object.</returns>
        public IRodsStream OpenRead()
        {
            return this.Open(FileMode.Open, FileAccess.Read);
        }

        /// <summary>
        /// Creates a write-only <see cref="IRodsStream"/>.
        /// </summary>
        /// <returns>A new write-only <see cref="IRodsStream"/> object.</returns>
        public IRodsStream OpenWrite()
        {
            return this.Open(FileMode.OpenOrCreate, FileAccess.Write);
        }

        public void Upload(Stream data, bool overwrite = false)
        {
            var maximumMessageDataSize = 32 * 1024 * 1024;

            var request = new DataObjectInputPacket(this.FullName);
            request.CreateMode = 33188;
            request.OpenFlag = 2;
            request.Offset = 0;
            request.DataSize = data.Length;
            request.NumOfThreads = 0;
            request.OperationType = OperationTypes.PUT_OPR;

            request.AddKey("dataType", "generic");
            request.AddKey("destRescName", string.IsNullOrEmpty(this.ResourceName) ? this.Connection.ResourceName : this.ResourceName);
            if (overwrite)
            {
                request.AddKey("forceFlag");            
            }

            if (data.Length < maximumMessageDataSize)
            {
                request.AddKey("dataIncluded");

                this.Connection.SendRequest(RequestCodes.DATA_OBJ_PUT_AN, request, data);
            }
            else
            {
                this.Connection.SendRequest(RequestCodes.DATA_OBJ_PUT_AN, request);
            }

            var response = this.Connection.ReceiveMessage<PortalOperationOutputPacket>();

            if (response.Exception != null)
            {
                throw response.Exception;
            }

            if (response.Body != null)
            {
                //  TODO:   Handle when threads count is 0, then ports will not be available to upload data , check....
                var numberOfThreads = response.Body.ThreadsCount;

                if (numberOfThreads < 1)
                    numberOfThreads = 1;

                var waitHandles = new WaitHandle[numberOfThreads];

                for (int i = 0; i < numberOfThreads; i++)
                {
                    EventWaitHandle threadCompleted = new AutoResetEvent(false);
                    waitHandles[i] = threadCompleted;
                    ThreadPool.QueueUserWorkItem(
                        new WaitCallback(this.UploadStreamData),
                        new StreamDataTaskInfo(response.Body.Port.Host, response.Body.Port.PortNumber, response.Body.Port.Cookie, response.Body.Port.WindowSize, data, threadCompleted));
                }

                WaitHandle.WaitAll(waitHandles);
            }

            this.Connection.SendRequest(RequestCodes.OPR_COMPLETE_AN, response.Body.Handle);

            var rr = this.Connection.ReceiveMessage();

            if (rr.Exception != null)
            {
                throw rr.Exception;
            }

        }

        public void Download(Stream output)
        {
            if (!output.CanWrite)
                throw new IRodsException("Cannot write into the stream.");

            var request = new DataObjectInputPacket(this.FullName);
            request.OperationType = OperationTypes.GET_OPR;

            //  If output stream cannot seek then use only one stream
            if (!output.CanSeek)
                request.NumOfThreads = -1;

            this.Connection.SendRequest(RequestCodes.DATA_OBJ_GET_AN, request);

            var response = this.Connection.ReceiveMessage<PortalOperationOutputPacket>();

            if (response.Exception != null)
            {
                throw response.Exception;
            }

            if (response.Body.ThreadsCount > 0)
            {
                var numberOfThreads = response.Body.ThreadsCount;

                if (numberOfThreads < 1)
                    numberOfThreads = 1;

                var waitHandles = new WaitHandle[numberOfThreads];

                for (int i = 0; i < numberOfThreads; i++)
                {
                    EventWaitHandle threadCompleted = new AutoResetEvent(false);
                    waitHandles[i] = threadCompleted;
                    ThreadPool.QueueUserWorkItem(
                        new WaitCallback(this.DownloadStreamData),
                        new StreamDataTaskInfo(response.Body.Port.Host, response.Body.Port.PortNumber, response.Body.Port.Cookie, response.Body.Port.WindowSize, output, threadCompleted));
                }

                WaitHandle.WaitAll(waitHandles);
            }
            else
            {
                OpenedDataObjectInputPacket a = new OpenedDataObjectInputPacket();
                a.Handle = response.Body.Handle;
                a.Length = 4 * 1024 * 1024;
                
                var buffer = new byte[a.Length];

                int bytesRead = 1;

                while (bytesRead > 0)
                {
                    this.Connection.SendRequest(RequestCodes.DATA_OBJ_READ_AN, a);

                    var r = this.Connection.ReceiveMessage();

                    r.BinaryData.Read(buffer, 0, bytesRead);

                    output.Write(buffer, 0, bytesRead);

                    bytesRead = (int)r.RequestCode;
                }
            }

            this.Connection.SendRequest(RequestCodes.OPR_COMPLETE_AN, response.Body.Handle);

            var rr = this.Connection.ReceiveMessage();

            if (rr.Exception != null)
            {
                throw rr.Exception;
            }
        }

        /// <summary>
        /// Moves a specified file to a new location, providing the option to specify a new file name.
        /// </summary>
        /// <param name="destFileName">The path to move the file to, which can specify a different file name.</param>
        public void MoveTo(string destFileName)
        {
            if (!this.Exists)
                throw new FileNotFoundException(string.Format("File '{0}' not found."));

            var request = new DataObjectCopyInputPacket(this.FullName, this.Connection.ParsePath(destFileName));

            request.Destination.AddKey("forceFlag");
            request.Source.OperationType = OperationTypes.RENAME_UNKNOWN_TYPE;

            this.Connection.SendRequest(RequestCodes.DATA_OBJ_RENAME_AN, request);

            var response = this.Connection.ReceiveMessage();

            if (response.Exception != null)
            {
                throw response.Exception;
            }
        }

        public override void Delete()
        {
            if (!this.Exists)
                throw new FileNotFoundException(string.Format("File '{0}' not found."));

            var request = new DataObjectInputPacket(this.FullName);

            this.Connection.SendRequest(RequestCodes.DATA_OBJ_RENAME_AN, request);

            var response = this.Connection.ReceiveMessage();

            if (response.Exception != null)
            {
                throw response.Exception;
            }
        }

        public override void Refresh()
        {
            this.Exists = this.Load((q) => { 
                q.AddCondition(DataObjectAttributes.COL_DATA_NAME, string.Format("='{0}'", this.Name));
                q.AddCondition(DataObjectAttributes.COL_COLL_NAME, string.Format("='{0}'", this.CollectionName)); 
                
            });
        }

        private void UploadStreamData(object stateInfo)
        {
            //  TODO:   Add error handling to this method and complete operation with the code if error occurred
            StreamDataTaskInfo ti = stateInfo as StreamDataTaskInfo;
            //  Open sockets
            //  TODO:   Enumerate thru AddressList to address IPv6 case
            var ep = new IPEndPoint(Dns.GetHostAddresses(ti.Host)[0], ti.Port);
            var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            socket.Connect(ep);

            //  rodsSetSockOpt
            var socketSize = ti.WindowSize;
            if (socketSize > 16 * 1024 * 1024)
            {
                socketSize = 16 * 1024 * 1024;
            }
            else if (socketSize < 1 * 1024 * 1024)
            {
                socketSize = 1 * 1024 * 1024;
            }

            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, socketSize);
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, socketSize);
            socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.NoDelay, true);
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, new LingerOption(true, 5));

            //  Send cookie
            var cookieBytes = new byte[] { (byte)(ti.Cookie >> 24), (byte)(ti.Cookie >> 16), (byte)(ti.Cookie >> 8), (byte)(ti.Cookie & 0xFF) };
            var sent = socket.Send(cookieBytes);

            if (sent != cookieBytes.Length)
            {
                //  TODO:   Handle this error as it could be running on different thread
                throw new InvalidOperationException();
            }

            while (true)
            {
                //  Receive header information
                var headerData = new byte[TransferHeader.BytesLength];
                var received = socket.Receive(headerData);
                TransferHeader header = new TransferHeader(headerData);

                if (header.OperationType == OperationTypes.DONE_OPR || header.Length == 0)
                    break;

                var bytesToRead = header.Length;
                var offset = header.Offset;

                var bufferSize = (bytesToRead > 4 * 1024 * 1024) ? 4 * 1024 * 1024 : bytesToRead;
                var buffer = new byte[bufferSize];
                sent = 0;
                while (bytesToRead > 0 && sent < header.Length)
                {
                    int read;
                    //  Read from stream
                    lock (ti.Data)
                    {
                        ti.Data.Seek(offset, SeekOrigin.Begin);
                        read = ti.Data.Read(buffer, 0, buffer.Length);
                    }

                    offset += read;

                    bytesToRead -= read;

                    //  TODO:   See why socket sometimes closes while trying to send data
                    sent = socket.Send(buffer, read, SocketFlags.None);
                }
            }

            socket.Close();

            ti.WaitHandle.Set();
        }

        private void DownloadStreamData(object stateInfo)
        {
            //  TODO:   Add error handling to this method and complete operation with the code if error occurred
            StreamDataTaskInfo ti = stateInfo as StreamDataTaskInfo;
            //  Open sockets
            //  TODO:   Enumerate thru AddressList to address IPv6 case
            var ep = new IPEndPoint(Dns.GetHostAddresses(ti.Host)[0], ti.Port);
            var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            socket.Connect(ep);

            //  rodsSetSockOpt
            var socketSize = ti.WindowSize;
            if (socketSize > 16 * 1024 * 1024)
            {
                socketSize = 16 * 1024 * 1024;
            }
            else if (socketSize < 1 * 1024 * 1024)
            {
                socketSize = 1 * 1024 * 1024;
            }

            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, socketSize);
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, socketSize);
            socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.NoDelay, true);
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, new LingerOption(true, 5));

            //  Send cookie
            var cookieBytes = new byte[] { (byte)(ti.Cookie >> 24), (byte)(ti.Cookie >> 16), (byte)(ti.Cookie >> 8), (byte)(ti.Cookie & 0xFF) };
            var sent = socket.Send(cookieBytes);

            if (sent != cookieBytes.Length)
            {
                //  TODO:   Handle this error as it could be running on different thread
                throw new InvalidOperationException();
            }

            while (true)
            {
                //  Receive header information
                var headerData = new byte[TransferHeader.BytesLength];
                var received = socket.Receive(headerData);
                TransferHeader header = new TransferHeader(headerData);

                if (header.OperationType == OperationTypes.DONE_OPR || header.Length == 0)
                    break;

                var bytesToWrite = header.Length;
                var offset = header.Offset;

                var bufferSize = (bytesToWrite > 4 * 1024 * 1024) ? 4 * 1024 * 1024 : bytesToWrite;
                var buffer = new byte[bufferSize];
                sent = 0;
                while (bytesToWrite > 0)
                {
                    int read = socket.Receive(buffer, 0, buffer.Length, SocketFlags.None);
                    
                    //  Read from stream
                    lock (ti.Data)
                    {
                        ti.Data.Seek(offset, SeekOrigin.Begin);
                        ti.Data.Write(buffer, 0, read);
                    }

                    offset += read;

                    bytesToWrite -= read;
                }
            }

            socket.Close();

            ti.WaitHandle.Set();
        }

        private class TransferHeader
        {
            public const int BytesLength = 4 + 4 + 8 + 8;

            public OperationTypes OperationType { get; private set; }

            public int Flags { get; private set; }

            public long Offset { get; private set; }

            public long Length { get; private set; }

            public TransferHeader(byte[] data)
            {
                this.OperationType = (OperationTypes)((int)(data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]));
                this.Flags = ((int)(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]));
                this.Offset = ((long)(data[8] << 56 | data[9] << 48 | data[10] << 40 | data[11] << 32 | data[12] << 24 | data[13] << 16 | data[14] << 8 | data[15]));
                this.Length = ((long)(data[16] << 56 | data[17] << 48 | data[18] << 40 | data[19] << 32 | data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23]));
            }
        }

        private class StreamDataTaskInfo
        {
            public string Host { get; private set; }

            public int Port { get; private set; }

            public int Cookie { get; private set; }

            public int WindowSize { get; private set; }

            public Stream Data { get; private set; }

            public EventWaitHandle WaitHandle { get; private set; }

            public StreamDataTaskInfo(string host, int port, int cookie, int windowSize, Stream data, EventWaitHandle waitHandle)
            {
                this.Host = host;
                this.Port = port;
                this.Cookie = cookie;
                this.WindowSize = windowSize;
                this.Data = data;
                this.WaitHandle = waitHandle;
            }
        }
    }
}
